{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "initial_id",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "提取到的 NTESSTUDYSI: beab0f3bda7846a380c27f7ea8d9a014\n",
      "RPC 请求响应：\n",
      "{'code': 0, 'result': {'query': {'sortCriterial': ' agree_count desc, gmt_modified desc,mark desc ', 'DEFAULT_PAGE_SIZE': 10, 'DEFAULT_PAGE_INDEX': 1, 'DEFAULT_TOTLE_PAGE_COUNT': 1, 'DEFAULT_TOTLE_COUNT': 0, 'DEFAULT_OFFSET': 0, 'pageSize': 20, 'pageIndex': 1, 'totlePageCount': 65, 'totleCount': 1293, 'offset': 0, 'limit': 20}, 'list': [{'id': 6753726, 'gmtModified': 1705826239453, 'commentorId': 1021440455, 'userNickName': 'jjxl2018', 'faceUrl': 'https://img-ph-mirror.nosdn.127.net/25AeHHpyQV9JxQ3BIpguKA==/6632196964140525554.jpg', 'content': '我是一名医学基础课程老师，负责病理生理学的教学，为了适应新时代的需求，我们学校开设了一个新兴医学专业“医 X”。在新医科以人工智能、大数据、云计算为代表的科技革命新背景下，本专业在病理生理学教学课程中也融入人工智能、大数据等相关内容。李老师的计算机课不仅给学生带来新的技术技能，也给我们带来很多启发，为实现培养“学科交叉、服务临床、注重创新、引领发展”的复合型医学人才做好了充分的准备。', 'mark': 5.0, 'courseId': None, 'termId': 1470746442, 'courseName': None, 'agreeCount': 10, 'status': 1, 'upvote': None, 'productType': None, 'shortName': None, 'courseMode': None}, {'id': 5020167, 'gmtModified': 1635822979535, 'commentorId': 1473496346, 'userNickName': 'mooc56490986133536953', 'faceUrl': '', 'content': '老师讲的太好了吧，人美又温柔，耐心解决问题，我学会了，yyds', 'mark': 5.0, 'courseId': None, 'termId': 1465386454, 'courseName': None, 'agreeCount': 6, 'status': 1, 'upvote': None, 'productType': None, 'shortName': None, 'courseMode': None}, {'id': 6622337, 'gmtModified': 1702880991040, 'commentorId': 1559803585, 'userNickName': '君辞2560', 'faceUrl': '', 'content': '非常好课程，使我的大脑旋转', 'mark': 5.0, 'courseId': None, 'termId': 1470746442, 'courseName': None, 'agreeCount': 5, 'status': 1, 'upvote': None, 'productType': None, 'shortName': None, 'courseMode': None}, {'id': 5338618, 'gmtModified': 1648121413168, 'commentorId': 1510872667, 'userNickName': 'mooc110061795291999110', 'faceUrl': '', 'content': '老师讲课生动有趣，带我们走进爬虫，让我又对计算机提起了兴趣(/≧▽≦)/~┴┴ (开心起来掀个桌没毛病吧～)', 'mark': 5.0, 'courseId': None, 'termId': 1467058488, 'courseName': None, 'agreeCount': 4, 'status': 1, 'upvote': None, 'productType': None, 'shortName': None, 'courseMode': None}, {'id': 6751643, 'gmtModified': 1705750846905, 'commentorId': 1528875780, 'userNickName': '乳酸钠林格', 'faceUrl': '', 'content': '非常不错的课程，虽说是线上课程，但是学习效果丝毫不比线下课得效果差，老师线上回复十分及时，快速上手Python简直不要太爽，可视化的学习也是对Python语言理解的加深，更深一步理解了市面上各种各样的优美的数据大屏的来源和制作方法，技能upupup～', 'mark': 5.0, 'courseId': None, 'termId': 1470746442, 'courseName': None, 'agreeCount': 3, 'status': 1, 'upvote': None, 'productType': None, 'shortName': None, 'courseMode': None}, {'id': 6621991, 'gmtModified': 1702967368813, 'commentorId': 1559951974, 'userNickName': 'under.', 'faceUrl': 'http://study-image.nosdn.127.net/member_sns_photo_aed372913eca48f7ac85255f317f361b?imageView&thumbnail=120y120&quality=100', 'content': '虽然我不是专门的计算机专业，面对代码这些东西会很有疑惑，但是李老师的授课方式和效果却是独一佳的，让原本无味的知识更加生动，易理解，课堂上也是十分轻松却也能够学到很多有用的知识，很喜欢李老师的课！！！', 'mark': 5.0, 'courseId': None, 'termId': 1470746442, 'courseName': None, 'agreeCount': 3, 'status': 1, 'upvote': None, 'productType': None, 'shortName': None, 'courseMode': None}, {'id': 6622967, 'gmtModified': 1702967136918, 'commentorId': 1559946372, 'userNickName': '半夏不当行', 'faceUrl': 'http://study-image.nosdn.127.net/member_sns_photo_68fdd5c7161445b9b721cdcca0799525?imageView&amp;thumbnail=120y120&amp;quality=100', 'content': '老师讲的很好，每个知识点都讲得很透彻，作为新手，感觉很清晰，特别是案例是一个完整的案例贯穿整个知识路线，比其他老师知识蜻蜓点水式的讲解好的不是一点半点，给老师点赞。', 'mark': 5.0, 'courseId': None, 'termId': 1470746442, 'courseName': None, 'agreeCount': 3, 'status': 1, 'upvote': None, 'productType': None, 'shortName': None, 'courseMode': None}, {'id': 6070739, 'gmtModified': 1679454752524, 'commentorId': 1512385316, 'userNickName': '颜康20229411118', 'faceUrl': 'http://mooc-image.nosdn.127.net/60FE3799B7A42DAF12C38C24D89EBA59.jpg?imageView&amp;thumbnail=310x205&amp;quality=100', 'content': '老师非常棒，很好的课程！', 'mark': 5.0, 'courseId': None, 'termId': 1469954449, 'courseName': None, 'agreeCount': 3, 'status': 1, 'upvote': None, 'productType': None, 'shortName': None, 'courseMode': None}, {'id': 5178198, 'gmtModified': 1641355678216, 'commentorId': 4421196, 'userNickName': 'cugzhaohui', 'faceUrl': 'https://nos.netease.com/edu-image/32a8dd78526e4ae6bedc2e5cc8a380b6.png', 'content': '老师讲的很好，每个知识点都讲得很透彻，作为新手，感觉很清晰，特别是案例是一个完整的案例贯穿整个知识路线，比其他老师知识蜻蜓点水式的讲解好的不是一点半点，给老师点赞。', 'mark': 5.0, 'courseId': None, 'termId': 1465386454, 'courseName': None, 'agreeCount': 3, 'status': 1, 'upvote': None, 'productType': None, 'shortName': None, 'courseMode': None}, {'id': 6831730, 'gmtModified': 1711348535930, 'commentorId': 1567184059, 'userNickName': '谢美丽飞飞飞', 'faceUrl': 'http://mooc-image.nosdn.127.net/82444165411B822565747727AECD2A96.jpg?imageView&amp;thumbnail=310x205&amp;quality=100', 'content': '老师超级好，传授的知识很懂，老师在很认真的教书，课堂气氛超级活跃，我要做一个大数据的医学生爬虫！！！', 'mark': 5.0, 'courseId': None, 'termId': 1472098477, 'courseName': None, 'agreeCount': 2, 'status': 1, 'upvote': None, 'productType': None, 'shortName': None, 'courseMode': None}, {'id': 6835690, 'gmtModified': 1711348336696, 'commentorId': 1564977878, 'userNickName': '天天保护地球', 'faceUrl': None, 'content': '这门课程能让我更好地认识计算机与python，帮助我利用计算机进行数据处理，查找文献，有效地利用好网上资源。', 'mark': 5.0, 'courseId': None, 'termId': 1472098477, 'courseName': None, 'agreeCount': 2, 'status': 1, 'upvote': None, 'productType': None, 'shortName': None, 'courseMode': None}, {'id': 6809728, 'gmtModified': 1710311685272, 'commentorId': 1486126215, 'userNickName': '墨摆摆', 'faceUrl': 'http://edu-image.nosdn.127.net/8EE834C3EC1FBC4332C68F398C53FC8B.jpg?imageView&amp;thumbnail=310x205&amp;quality=100', 'content': '学习爬虫课程是一种非常有趣和实用的经历。在这门课程中，我学到了如何使用编程语言和相关工具来自动化获取互联网上的数据。\\n首先，学习爬虫课程让我更深入地了解了互联网的工作原理。我学会了如何发送HTTP请求并解析返回的网页内容，了解了网页的结构和标记语言，如HTML和CSS。通过学习网页的结构，我可以更好地理解如何提取我需要的数据。\\n其次，学习爬虫课程使我掌握了一些常用的爬虫工具和技术。我学会了使用Python编程语言来编写爬虫程序，使用相关库，如Requests和BeautifulSoup，来实现数据的获取和解析。我也学会了处理一些常见的爬虫问题，如处理动态网页生成的数据、处理反爬虫机制等。\\n最重要的是，学习爬虫课程让我认识到数据隐私和伦理问题的重要性。在抓取数据的过程中，我们需要遵守网站的使用条款和隐私政策，并尊重数据的所有权和隐私。学习爬虫课程也提醒了我要学会合理使用和处理数据，避免滥用和侵犯他人的隐私。\\n', 'mark': 5.0, 'courseId': None, 'termId': 1472098477, 'courseName': None, 'agreeCount': 2, 'status': 1, 'upvote': None, 'productType': None, 'shortName': None, 'courseMode': None}, {'id': 6992784, 'gmtModified': 1718774525651, 'commentorId': 1566690912, 'userNickName': 'mooc173942998684223685', 'faceUrl': '', 'content': '老师讲的特别好，讲的特别细致，平时有问题也会及时回复，回复得也特别容易理解，真不错！', 'mark': 5.0, 'courseId': None, 'termId': 1472098477, 'courseName': None, 'agreeCount': 1, 'status': 1, 'upvote': None, 'productType': None, 'shortName': None, 'courseMode': None}, {'id': 6992328, 'gmtModified': 1718630452847, 'commentorId': 1566203155, 'userNickName': 'mooc10800937207421045', 'faceUrl': '', 'content': '老师讲的很生动有趣，很容易听会。', 'mark': 5.0, 'courseId': None, 'termId': 1472098477, 'courseName': None, 'agreeCount': 1, 'status': 1, 'upvote': None, 'productType': None, 'shortName': None, 'courseMode': None}, {'id': 6996263, 'gmtModified': 1718620539049, 'commentorId': 1569983616, 'userNickName': '何鸣涛', 'faceUrl': 'http://study-image.nosdn.127.net/member_sns_photo_d2a1d2af8b6341d7837d74a4b1907e85?imageView&amp;thumbnail=120y120&amp;quality=100', 'content': '课程内容中使老师讲解详细举例生动，是学生容易理解老师的课程安排非常合理，能够让学生循序渐进的学进知识\\n', 'mark': 5.0, 'courseId': None, 'termId': 1472098477, 'courseName': None, 'agreeCount': 1, 'status': 1, 'upvote': None, 'productType': None, 'shortName': None, 'courseMode': None}, {'id': 6831736, 'gmtModified': 1711349495536, 'commentorId': 1480640062, 'userNickName': '零伍零九', 'faceUrl': 'http://edu-image.nosdn.127.net/85ce4b913e634e8e9c73a9af70b9dc19?imageView&thumbnail=120y120&quality=100', 'content': '老师认真且负责，让我们看到了计算机与医学的结合，给我们提供了不同的视角。', 'mark': 5.0, 'courseId': None, 'termId': 1472098477, 'courseName': None, 'agreeCount': 1, 'status': 1, 'upvote': None, 'productType': None, 'shortName': None, 'courseMode': None}, {'id': 6832676, 'gmtModified': 1711348823991, 'commentorId': 1567183110, 'userNickName': 'mooc11914815852490750', 'faceUrl': '', 'content': '我是一名临床医学的学生，正在学习Python数据爬取的内容。在新医科背景下，学科交叉，医工融合是时代大趋势，数据爬取对于医学领域论文写作来说无疑是一本葵花宝典，倚天屠龙剑。学习Python难免会遇到许多挫折和疑问，李老师的课程带我走出疑惑，如拨云见日，受益匪浅。', 'mark': 5.0, 'courseId': None, 'termId': 1472098477, 'courseName': None, 'agreeCount': 1, 'status': 1, 'upvote': None, 'productType': None, 'shortName': None, 'courseMode': None}, {'id': 6835695, 'gmtModified': 1711348436651, 'commentorId': 1567182130, 'userNickName': 'mooc11740121521871021', 'faceUrl': None, 'content': 'Python课程深入浅出，老师教学经验丰富，讲解清晰易懂，案例丰富实用，让人受益匪浅，强烈推荐给对编程感兴趣的朋友！', 'mark': 5.0, 'courseId': None, 'termId': 1472098477, 'courseName': None, 'agreeCount': 1, 'status': 1, 'upvote': None, 'productType': None, 'shortName': None, 'courseMode': None}, {'id': 6837674, 'gmtModified': 1711348347035, 'commentorId': 1549014049, 'userNickName': '学员53eakada111726467573635545', 'faceUrl': 'http://edu-image.nosdn.127.net/bad68f50973742829bffc3172c04f36c?imageView&amp;thumbnail=120y120&amp;quality=100', 'content': '以前用电脑效率很低，耗费大量时间，学习了Python之后，爬取数据很快，对于医学生很有帮助', 'mark': 5.0, 'courseId': None, 'termId': 1472098477, 'courseName': None, 'agreeCount': 1, 'status': 1, 'upvote': None, 'productType': None, 'shortName': None, 'courseMode': None}, {'id': 6831422, 'gmtModified': 1711159643988, 'commentorId': 1486914134, 'userNickName': '还得等会哈', 'faceUrl': 'http://edu-image.nosdn.127.net/d474b24a949b41669427e86a21beed14?imageView&amp;thumbnail=120y120&amp;quality=100', 'content': '老师讲的很好，每个知识点都讲得很透彻，作为新手，感觉很清晰，特别是案例是一个完整的案例贯穿整个知识路线，比其他老师知识蜻蜓点水式的讲解好的不是一点半点，给老师点赞。', 'mark': 5.0, 'courseId': None, 'termId': 1472098477, 'courseName': None, 'agreeCount': 1, 'status': 1, 'upvote': None, 'productType': None, 'shortName': None, 'courseMode': None}]}, 'message': '', 'traceId': '', 'sampled': False}\n",
      "评论数据已保存到 course_comments.csv\n"
     ]
    }
   ],
   "source": [
    "import requests\n",
    "import pandas as pd\n",
    "\n",
    "def get_ntesstudysi(session, course_url):\n",
    "    \"\"\"\n",
    "    请求课程页面并提取 NTESSTUDYSI Cookie 值。\n",
    "\n",
    "    :param session: requests.Session 对象，用于保持会话。\n",
    "    :param course_url: 课程页面的 URL。\n",
    "    :return: NTESSTUDYSI 的值。\n",
    "    \"\"\"\n",
    "    headers = {\n",
    "        \"User-Agent\": (\n",
    "            \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) \"\n",
    "            \"AppleWebKit/537.36 (KHTML, like Gecko) \"\n",
    "            \"Chrome/128.0.0.0 Safari/537.36\"\n",
    "        ),\n",
    "        \"Accept\": (\n",
    "            \"text/html,application/xhtml+xml,application/xml;\"\n",
    "            \"q=0.9,image/webp,image/apng,*/*;q=0.8\"\n",
    "        ),\n",
    "        \"Accept-Language\": \"zh-CN,zh;q=0.9\",\n",
    "    }\n",
    "\n",
    "    response = session.get(course_url, headers=headers)\n",
    "    response.raise_for_status()\n",
    "\n",
    "    # 提取 NTESSTUDYSI Cookie\n",
    "    ntesstudysi = session.cookies.get('NTESSTUDYSI')\n",
    "    if not ntesstudysi:\n",
    "        raise ValueError(\"未找到 NTESSTUDYSI Cookie。请检查请求是否成功或 Cookie 名称是否正确。\")\n",
    "\n",
    "    return ntesstudysi\n",
    "\n",
    "def make_rpc_request(session, rpc_url_base, ntesstudysi, payload):\n",
    "    \"\"\"\n",
    "    使用 NTESSTUDYSI 作为 csrfKey 发起 RPC 请求。\n",
    "\n",
    "    :param session: requests.Session 对象，用于保持会话。\n",
    "    :param rpc_url_base: RPC 请求的基础 URL（不包含 csrfKey）。\n",
    "    :param ntesstudysi: NTESSTUDYSI 的值，用作 csrfKey。\n",
    "    :param payload: POST 请求的负载（字符串格式）。\n",
    "    :return: RPC 请求的 JSON 响应。\n",
    "    \"\"\"\n",
    "    # 构建完整的 RPC 请求 URL，包含 csrfKey 参数\n",
    "    rpc_url = f\"{rpc_url_base}?csrfKey={ntesstudysi}\"\n",
    "\n",
    "    headers = {\n",
    "        \"User-Agent\": (\n",
    "            \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) \"\n",
    "            \"AppleWebKit/537.36 (KHTML, like Gecko) \"\n",
    "            \"Chrome/128.0.0.0 Safari/537.36\"\n",
    "        ),\n",
    "        \"Accept\": \"*/*\",\n",
    "        \"Accept-Language\": \"zh-CN,zh;q=0.9\",\n",
    "        \"Content-Type\": \"application/x-www-form-urlencoded\",\n",
    "        \"Priority\": \"u=1, i\",\n",
    "        \"Sec-CH-UA\": '\"Chromium\";v=\"128\", \"Not;A=Brand\";v=\"24\", \"Google Chrome\";v=\"128\"',\n",
    "        \"Sec-CH-UA-Mobile\": \"?0\",\n",
    "        \"Sec-CH-UA-Platform\": '\"Windows\"',\n",
    "        \"Sec-Fetch-Dest\": \"empty\",\n",
    "        \"Sec-Fetch-Mode\": \"cors\",\n",
    "        \"Sec-Fetch-Site\": \"same-origin\",\n",
    "        \"Referer\": \"https://www.icourse163.org/course/NHDX-1463126169\",\n",
    "        \"Origin\": \"https://www.icourse163.org\",\n",
    "        \"Connection\": \"keep-alive\",\n",
    "    }\n",
    "\n",
    "    data = payload  # 负载为字符串格式，例如 \"courseId=1463126169&pageIndex=1&pageSize=20&orderBy=3\"\n",
    "\n",
    "    response = session.post(rpc_url, headers=headers, data=data)\n",
    "    response.raise_for_status()\n",
    "\n",
    "    # 尝试解析 JSON 响应\n",
    "    try:\n",
    "        return response.json()\n",
    "    except ValueError:\n",
    "        raise ValueError(\"响应不是有效的 JSON 格式。\")\n",
    "\n",
    "def parse_comments(json_response):\n",
    "    \"\"\"\n",
    "    解析 RPC 请求的 JSON 响应并提取评论数据。\n",
    "\n",
    "    :param json_response: RPC 请求的 JSON 响应。\n",
    "    :return: 提取到的评论列表。\n",
    "    \"\"\"\n",
    "    comments = []\n",
    "    if json_response and 'result' in json_response and 'list' in json_response['result']:\n",
    "        for comment in json_response['result']['list']:\n",
    "            comment_data = {\n",
    "                'comment_id': comment.get('id'),\n",
    "                'user_nickname': comment.get('userNickName'),\n",
    "                'content': comment.get('content'),\n",
    "                'agree_count': comment.get('agreeCount'),\n",
    "                'mark': comment.get('mark'),\n",
    "                'gmt_modified': comment.get('gmtModified')\n",
    "            }\n",
    "            comments.append(comment_data)\n",
    "    return comments\n",
    "\n",
    "def save_comments_to_csv(comments, file_name):\n",
    "    \"\"\"\n",
    "    将提取的评论数据保存到 CSV 文件。\n",
    "\n",
    "    :param comments: 评论数据列表。\n",
    "    :param file_name: 保存的 CSV 文件名。\n",
    "    \"\"\"\n",
    "    df = pd.DataFrame(comments)\n",
    "    df.to_csv(file_name, index=False, encoding='utf-8-sig')\n",
    "    print(f\"评论数据已保存到 {file_name}\")\n",
    "\n",
    "def main():\n",
    "    course_url = \"https://www.icourse163.org/course/NHDX-1463126169\"\n",
    "    rpc_url_base = \"https://www.icourse163.org/web/j/mocCourseV2RpcBean.getCourseEvaluatePaginationByCourseIdOrTermId.rpc\"\n",
    "\n",
    "    # POST 请求的负载\n",
    "    payload = \"courseId=1463126169&pageIndex=1&pageSize=20&orderBy=3\"\n",
    "\n",
    "    with requests.Session() as session:\n",
    "        try:\n",
    "            # 步骤 1：请求课程页面并提取 NTESSTUDYSI Cookie\n",
    "            ntesstudysi = get_ntesstudysi(session, course_url)\n",
    "            print(f\"提取到的 NTESSTUDYSI: {ntesstudysi}\")\n",
    "\n",
    "            # 步骤 2：使用 NTESSTUDYSI 作为 csrfKey 发起 RPC 请求\n",
    "            response = make_rpc_request(session, rpc_url_base, ntesstudysi, payload)\n",
    "            print(\"RPC 请求响应：\")\n",
    "            print(response)\n",
    "\n",
    "            # 步骤 3：解析评论数据\n",
    "            comments = parse_comments(response)\n",
    "\n",
    "            # 步骤 4：将解析后的评论数据保存到 CSV 文件\n",
    "            save_comments_to_csv(comments, 'course_comments.csv')\n",
    "\n",
    "        except Exception as e:\n",
    "            print(f\"发生错误: {e}\")\n",
    "\n",
    "if __name__ == \"__main__\":\n",
    "    main()\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "921daa87-cc1b-4407-99b0-c12a2291c1e6",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "提取到的 NTESSTUDYSI: 58166aeca4f94794aa693921a82f4c84\n",
      "正在提取第 1 页评论数据...\n",
      "正在提取第 2 页评论数据...\n",
      "正在提取第 3 页评论数据...\n",
      "正在提取第 4 页评论数据...\n",
      "正在提取第 5 页评论数据...\n",
      "正在提取第 6 页评论数据...\n",
      "正在提取第 7 页评论数据...\n",
      "正在提取第 8 页评论数据...\n",
      "正在提取第 9 页评论数据...\n",
      "正在提取第 10 页评论数据...\n",
      "正在提取第 11 页评论数据...\n",
      "正在提取第 12 页评论数据...\n",
      "正在提取第 13 页评论数据...\n",
      "正在提取第 14 页评论数据...\n",
      "正在提取第 15 页评论数据...\n",
      "正在提取第 16 页评论数据...\n",
      "正在提取第 17 页评论数据...\n",
      "正在提取第 18 页评论数据...\n",
      "正在提取第 19 页评论数据...\n",
      "正在提取第 20 页评论数据...\n",
      "正在提取第 21 页评论数据...\n",
      "正在提取第 22 页评论数据...\n",
      "正在提取第 23 页评论数据...\n",
      "正在提取第 24 页评论数据...\n",
      "正在提取第 25 页评论数据...\n",
      "正在提取第 26 页评论数据...\n",
      "正在提取第 27 页评论数据...\n",
      "正在提取第 28 页评论数据...\n",
      "正在提取第 29 页评论数据...\n",
      "正在提取第 30 页评论数据...\n",
      "正在提取第 31 页评论数据...\n",
      "正在提取第 32 页评论数据...\n",
      "正在提取第 33 页评论数据...\n",
      "正在提取第 34 页评论数据...\n",
      "正在提取第 35 页评论数据...\n",
      "正在提取第 36 页评论数据...\n",
      "正在提取第 37 页评论数据...\n",
      "正在提取第 38 页评论数据...\n",
      "正在提取第 39 页评论数据...\n",
      "正在提取第 40 页评论数据...\n",
      "正在提取第 41 页评论数据...\n",
      "正在提取第 42 页评论数据...\n",
      "正在提取第 43 页评论数据...\n",
      "正在提取第 44 页评论数据...\n",
      "正在提取第 45 页评论数据...\n",
      "正在提取第 46 页评论数据...\n",
      "正在提取第 47 页评论数据...\n",
      "正在提取第 48 页评论数据...\n",
      "正在提取第 49 页评论数据...\n",
      "正在提取第 50 页评论数据...\n",
      "正在提取第 51 页评论数据...\n",
      "正在提取第 52 页评论数据...\n",
      "正在提取第 53 页评论数据...\n",
      "正在提取第 54 页评论数据...\n",
      "正在提取第 55 页评论数据...\n",
      "正在提取第 56 页评论数据...\n",
      "正在提取第 57 页评论数据...\n",
      "正在提取第 58 页评论数据...\n",
      "正在提取第 59 页评论数据...\n",
      "正在提取第 60 页评论数据...\n",
      "正在提取第 61 页评论数据...\n",
      "正在提取第 62 页评论数据...\n",
      "正在提取第 63 页评论数据...\n",
      "正在提取第 64 页评论数据...\n",
      "正在提取第 65 页评论数据...\n",
      "评论数据已保存到 all_course_comments.csv\n"
     ]
    }
   ],
   "source": [
    "import requests\n",
    "import pandas as pd\n",
    "\n",
    "def get_ntesstudysi(session, course_url):\n",
    "    \"\"\"\n",
    "    请求课程页面并提取 NTESSTUDYSI Cookie 值。\n",
    "\n",
    "    :param session: requests.Session 对象，用于保持会话。\n",
    "    :param course_url: 课程页面的 URL。\n",
    "    :return: NTESSTUDYSI 的值。\n",
    "    \"\"\"\n",
    "    headers = {\n",
    "        \"User-Agent\": (\n",
    "            \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) \"\n",
    "            \"AppleWebKit/537.36 (KHTML, like Gecko) \"\n",
    "            \"Chrome/128.0.0.0 Safari/537.36\"\n",
    "        ),\n",
    "        \"Accept\": (\n",
    "            \"text/html,application/xhtml+xml,application/xml;\"\n",
    "            \"q=0.9,image/webp,image/apng,*/*;q=0.8\"\n",
    "        ),\n",
    "        \"Accept-Language\": \"zh-CN,zh;q=0.9\",\n",
    "    }\n",
    "\n",
    "    response = session.get(course_url, headers=headers)\n",
    "    response.raise_for_status()\n",
    "\n",
    "    # 提取 NTESSTUDYSI Cookie\n",
    "    ntesstudysi = session.cookies.get('NTESSTUDYSI')\n",
    "    if not ntesstudysi:\n",
    "        raise ValueError(\"未找到 NTESSTUDYSI Cookie。请检查请求是否成功或 Cookie 名称是否正确。\")\n",
    "\n",
    "    return ntesstudysi\n",
    "\n",
    "def make_rpc_request(session, rpc_url_base, ntesstudysi, payload):\n",
    "    \"\"\"\n",
    "    使用 NTESSTUDYSI 作为 csrfKey 发起 RPC 请求。\n",
    "\n",
    "    :param session: requests.Session 对象，用于保持会话。\n",
    "    :param rpc_url_base: RPC 请求的基础 URL（不包含 csrfKey）。\n",
    "    :param ntesstudysi: NTESSTUDYSI 的值，用作 csrfKey。\n",
    "    :param payload: POST 请求的负载（字符串格式）。\n",
    "    :return: RPC 请求的 JSON 响应。\n",
    "    \"\"\"\n",
    "    # 构建完整的 RPC 请求 URL，包含 csrfKey 参数\n",
    "    rpc_url = f\"{rpc_url_base}?csrfKey={ntesstudysi}\"\n",
    "\n",
    "    headers = {\n",
    "        \"User-Agent\": (\n",
    "            \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) \"\n",
    "            \"AppleWebKit/537.36 (KHTML, like Gecko) \"\n",
    "            \"Chrome/128.0.0.0 Safari/537.36\"\n",
    "        ),\n",
    "        \"Accept\": \"*/*\",\n",
    "        \"Accept-Language\": \"zh-CN,zh;q=0.9\",\n",
    "        \"Content-Type\": \"application/x-www-form-urlencoded\",\n",
    "        \"Priority\": \"u=1, i\",\n",
    "        \"Sec-CH-UA\": '\"Chromium\";v=\"128\", \"Not;A=Brand\";v=\"24\", \"Google Chrome\";v=\"128\"',\n",
    "        \"Sec-CH-UA-Mobile\": \"?0\",\n",
    "        \"Sec-CH-UA-Platform\": '\"Windows\"',\n",
    "        \"Sec-Fetch-Dest\": \"empty\",\n",
    "        \"Sec-Fetch-Mode\": \"cors\",\n",
    "        \"Sec-Fetch-Site\": \"same-origin\",\n",
    "        \"Referer\": \"https://www.icourse163.org/course/NHDX-1463126169\",\n",
    "        \"Origin\": \"https://www.icourse163.org\",\n",
    "        \"Connection\": \"keep-alive\",\n",
    "    }\n",
    "\n",
    "    response = session.post(rpc_url, headers=headers, data=payload)\n",
    "    response.raise_for_status()\n",
    "\n",
    "    # 尝试解析 JSON 响应\n",
    "    try:\n",
    "        return response.json()\n",
    "    except ValueError:\n",
    "        raise ValueError(\"响应不是有效的 JSON 格式。\")\n",
    "\n",
    "def parse_comments(json_response):\n",
    "    \"\"\"\n",
    "    解析 RPC 请求的 JSON 响应并提取评论数据。\n",
    "\n",
    "    :param json_response: RPC 请求的 JSON 响应。\n",
    "    :return: 提取到的评论列表。\n",
    "    \"\"\"\n",
    "    comments = []\n",
    "    if json_response and 'result' in json_response and 'list' in json_response['result']:\n",
    "        for comment in json_response['result']['list']:\n",
    "            comment_data = {\n",
    "                'comment_id': comment.get('id'),\n",
    "                'user_nickname': comment.get('userNickName'),\n",
    "                'content': comment.get('content'),\n",
    "                'agree_count': comment.get('agreeCount'),\n",
    "                'mark': comment.get('mark'),\n",
    "                'gmt_modified': comment.get('gmtModified')\n",
    "            }\n",
    "            comments.append(comment_data)\n",
    "    return comments\n",
    "\n",
    "def save_comments_to_csv(comments, file_name):\n",
    "    \"\"\"\n",
    "    将提取的评论数据保存到 CSV 文件。\n",
    "\n",
    "    :param comments: 评论数据列表。\n",
    "    :param file_name: 保存的 CSV 文件名。\n",
    "    \"\"\"\n",
    "    df = pd.DataFrame(comments)\n",
    "    df.to_csv(file_name, index=False, encoding='utf-8-sig')\n",
    "    print(f\"评论数据已保存到 {file_name}\")\n",
    "\n",
    "def main():\n",
    "    course_url = \"https://www.icourse163.org/course/NHDX-1463126169\"\n",
    "    rpc_url_base = \"https://www.icourse163.org/web/j/mocCourseV2RpcBean.getCourseEvaluatePaginationByCourseIdOrTermId.rpc\"\n",
    "    total_pages = 65  # 总页数\n",
    "    all_comments = []  # 用于存储所有页的评论\n",
    "\n",
    "    with requests.Session() as session:\n",
    "        try:\n",
    "            # 步骤 1：请求课程页面并提取 NTESSTUDYSI Cookie\n",
    "            ntesstudysi = get_ntesstudysi(session, course_url)\n",
    "            print(f\"提取到的 NTESSTUDYSI: {ntesstudysi}\")\n",
    "\n",
    "            # 步骤 2：循环请求每一页的评论数据\n",
    "            for page in range(1, total_pages + 1):\n",
    "                payload = f\"courseId=1463126169&pageIndex={page}&pageSize=20&orderBy=3\"\n",
    "                response = make_rpc_request(session, rpc_url_base, ntesstudysi, payload)\n",
    "                print(f\"正在提取第 {page} 页评论数据...\")\n",
    "\n",
    "                # 解析每一页的评论并将其加入 all_comments 列表\n",
    "                comments = parse_comments(response)\n",
    "                all_comments.extend(comments)\n",
    "\n",
    "            # 步骤 3：将所有页的评论数据保存到 CSV 文件\n",
    "            save_comments_to_csv(all_comments, 'all_course_comments.csv')\n",
    "\n",
    "        except Exception as e:\n",
    "            print(f\"发生错误: {e}\")\n",
    "\n",
    "if __name__ == \"__main__\":\n",
    "    main()\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "ebd755a0-9cc0-4a7a-901e-f6cd97200038",
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.12.4"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
